<!DOCTYPE html>
<html>
  <head>
    <title>Sign in to CNPM</title>
    <style>
      body, div, p, form, h1, h2, input {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
      }
      input {
        line-height: normal;
        outline: none;
      }
      button {
        border-style: none;
      }
      body {
        position: relative;
        min-height: 100vh;
        background: #f7f7f7;
      }
      .main {
        display: -ms-flexbox;
        display: flex;
        -ms-flex-direction: column;
        flex-direction: column;
        margin: 0 auto;
        max-width: 27em;
      }
      .board {
        margin-top: 160px;
        padding: 40px;
        box-sizing: border-box;
        border-radius: 8px;
        background: #fff;
        box-shadow: 0 0 60px rgb(84 89 104 / 5%);
        text-align: center;
      }
      #login {
        margin-top: 16px;
      }
      #username, #password {
        display: block;
        padding: 6.5px 11px;
        margin-bottom: 24px;
        width: 100%;
        height: 38px;
        font-size: 16px;
        border: 1px solid #eee;
        border-radius: 4px;
        background: #f5f6f7;
      }
      #username:focus, #password:focus {
        border-color: #618eff;
      }
      #unbindWan {
        display: none;
        margin-bottom: 24px;
        padding-left: 4px;
        font-size: 14px;
        text-align: left;
        color: #555;
        -webkit-touch-callout: none;
        -webkit-user-select: none;
        -khtml-user-select: none;
        -moz-user-select: none;
        -ms-user-select: none;
        user-select: none;
      }
      #unbindWanCheckbox {
        vertical-align: middle;
        margin-bottom: 3px;
      }
      #submit {
        width: 100%;
        border-radius: 4px;
        height: 42px;
        margin: 0 2px;
        background: #215ae5;
        border-color: #215ae5;
        color: #fff;
        font-size: 16px;
        transition: all .3s cubic-bezier(.645, .045, .355, 1);
        cursor: pointer;
      }
      #submit:hover, #submit:focus {
        background: #497ef2;
        border-color: #497ef2;
      }
      #submit:active {
        background: #113dbf;
        border-color: #113dbf;
      }
      #submit:disabled {
        color: rgba(0, 0, 0, .25);
        background-image: none;
        background-color: rgba(0, 0, 0, .075);
        border-color: rgb(232, 232, 232);
        cursor: default;
      }
      #error_message {
        min-height: 22px;
        margin-bottom: 8px;
        font-size: 14px;
        color: red;
      }
    </style>
    <script>
      (function () {
        const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
        // Use a lookup table to find the index.
        const lookup = new Uint8Array(256);
        for (let i = 0; i < chars.length; i++) {
          lookup[chars.charCodeAt(i)] = i;
        }
        const encode = function (arraybuffer) {
          const bytes = new Uint8Array(arraybuffer);
          const len = bytes.length;
          let base64 = '';
          for (let i = 0; i < len; i += 3) {
            base64 += chars[bytes[i] >> 2];
            base64 += chars[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)];
            base64 += chars[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)];
            base64 += chars[bytes[i + 2] & 63];
          }
          if (len % 3 === 2) {
            base64 = base64.substring(0, base64.length - 1);
          } else if (len % 3 === 1) {
            base64 = base64.substring(0, base64.length - 2);
          }
          return base64;
        };
        const decode = function (base64) {
          const len = base64.length;
          const bufferLength = base64.length * 0.75;
          const arraybuffer = new ArrayBuffer(bufferLength);
          const bytes = new Uint8Array(arraybuffer);
          let p = 0;
          for (let i = 0; i < len; i += 4) {
            const encoded1 = lookup[base64.charCodeAt(i)];
            const encoded2 = lookup[base64.charCodeAt(i + 1)];
            const encoded3 = lookup[base64.charCodeAt(i + 2)];
            const encoded4 = lookup[base64.charCodeAt(i + 3)];
            bytes[p++] = (encoded1 << 2) | (encoded2 >> 4);
            bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2);
            bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63);
          }
          return arraybuffer;
        };
        window.base64url = { encode, decode };
      })();
    </script>
  </head>
  <body>
    <div class="main">
      <div class="board">
        <h2 class="title">Sign in to CNPM</h2>
        <form id="login">
          <p id="error_message"></p>
          <input type="hidden" id="sessionId" value="{{sessionId}}" />
          <input type="hidden" id="publicKey" value="{{publicKey}}" />
          <input type="hidden" id="enableWebauthn" value="{{enableWebauthn}}" />
          <input type="text" id="username" minlength="1" maxlength="100" placeholder="Username" autocomplete="username webauthn" />
          {% if enableWebauthn === true %}
          <input style="display: none" type="password" id="password" minlength="8" maxlength="100" placeholder="Password" autocomplete="current-password" oncontextmenu="return false" onpaste="return false" oncopy="return false" oncut="return false" />
          <div id="unbindWan">
            <input type="checkbox" id="unbindWanCheckbox" />
            <label for="unbindWanCheckbox">Unbind Touch ID/Face ID</label>
          </div>
          {% else %}
          <input type="password" id="password" minlength="8" maxlength="100" placeholder="Password" autocomplete="current-password" oncontextmenu="return false" onpaste="return false" oncopy="return false" oncut="return false" />
          {% endif %}
        </form>
        <button id="submit" disabled="true">Sign in</button>
      </div>
    </div>
    <script src="https://registry.npmmirror.com/jsencrypt/3.3.2/files/bin/jsencrypt.min.js"></script>
    <script src="https://registry.npmmirror.com/jquery/3.6.3/files/dist/jquery.min.js"></script>
    <script>
      $(function() {
        const $submitBtn = $('#submit');
        const $username = $('#username');
        const $password = $('#password');
        const $publicKey = $('#publicKey');
        const $errorMessage = $('#error_message');
        const $enableWebauthn = $('#enableWebauthn');
        const enableWebauthn = $enableWebauthn.val() === 'true';
        // macOS Firefox not support
        const ua = window.navigator.userAgent.toLowerCase();
        const isMac = /mac os/.test(ua);
        const isFirefox = ua.indexOf('firefox') > -1;
        const isSupportWebauthn = window.PublicKeyCredential && (!isMac || !isFirefox) && enableWebauthn;
        let preapreData = {};

        $username.focus();
        $username.keyup((e) => {
          if (e.keyCode === 13) $username.blur();
        });
        $username.on('blur', () => {
          const name = $username.val().trim();
          $username.val(name);
          if (!name || name.length > 100) return;
          handlePrepare(name);
        });
        $username.on('input', () => {
          validInput();
        });
        $password.on('input', () => {
          validInput();
        });
        $password.keyup((e) => {
          if (e.keyCode === 13 && validInput()) $submitBtn.click();
        });
        $submitBtn.on('click', () => {
          const name = $username.val().trim();
          const password = $password.val();
          const publicKey = $publicKey.val();
          const accData = {
            username: name,
            password: password ? encryptRSA(publicKey, password) : '',
          };
          if (!isSupportWebauthn || !preapreData.wanCredentialRegiOption) {
            handleSubmit({ accData, needUnbindWan: $('#unbindWanCheckbox').is(':checked') });
            return;
          }
          handleRegistration(preapreData.wanCredentialRegiOption, {
            success(data) {
              handleSubmit({ accData, wanCredentialRegiData: data });
            },
            fail() {
              handleSubmit({ accData });
            },
          });
        });

        function handlePrepare(name) {
          $.get('/-/v1/login/request/prepare/{{sessionId}}?name=' + name)
            .done(res => {
              preapreData = res;
              setSignTextByStatus(res.wanStatus);
              // webauthn unbound or user not found
              if (!isSupportWebauthn || res.wanStatus !== 2 || !res.wanCredentialAuthOption) {
                showPasswordAndFocus();
                return;
              }
              // webauthn bound
              handleAuthentication(res.wanCredentialAuthOption, {
                success(data) {
                  handleSubmit({ accData: { username: name }, wanCredentialAuthData: data });
                },
                fail(err) {
                  showPasswordAndFocus();
                  $('#unbindWan').show();
                },
              });
            })
            .fail(() => {
              showPasswordAndFocus();
            });
        }
        function handleSubmit(options) {
          $.ajax({
            url: '/-/v1/login/request/session/{{sessionId}}',
            type: 'POST',
            data: JSON.stringify(options),
            contentType: 'application/json; charset=utf-8',
            dataType: 'json',
          })
            .done(res => {
              if (res.ok) {
                window.location.replace('/-/v1/login/request/success');
              } else {
                $errorMessage.html(res.message);
              }
            })
            .fail(err => {
              $errorMessage.html(err.message);
            });
        }
        function handleRegistration(data, { success, fail }) {
          data.challenge = base64url.decode(data.challenge);
          data.user.id = base64url.decode(data.user.id);
          try {
            navigator.credentials.create({
              publicKey: data,
            }).then(credential => {
              const result = {
                id: credential.id,
                rawId: base64url.encode(credential.rawId),
                response: {
                  clientDataJSON: base64url.encode(credential.response.clientDataJSON),
                  attestationObject: base64url.encode(credential.response.attestationObject),
                },
                type: credential.type,
              };
              success && success(result);
            }).catch(err => {
              fail && fail(err);
            });
          } catch (err) {
            fail && fail(err);
          }
        }
        function handleAuthentication(data, { success, fail }) {
          data.challenge = base64url.decode(data.challenge);
          data.allowCredentials.forEach(c => {
            c.id = base64url.decode(c.id);
          });
          try {
            navigator.credentials.get({
              publicKey: data,
            }).then(credential => {
              const result = {
                id: credential.id,
                rawId: base64url.encode(credential.rawId),
                response: {
                  authenticatorData: base64url.encode(credential.response.authenticatorData),
                  clientDataJSON: base64url.encode(credential.response.clientDataJSON),
                  signature: base64url.encode(credential.response.signature),
                  userHandle: credential.response.userHandle ? base64url.encode(credential.response.userHandle) : undefined,
                },
                type: credential.type,
              };
              success && success(result);
            }).catch(err => {
              fail && fail(err);
            });
          } catch (err) {
            fail && fail(err);
          }
        }
        function validInput() {
          var uval = $username.val().trim();
          var spval = $password.val();
          var shouldDisabledSubmit = !uval || uval.length > 100 || spval.length < 8 || spval.length > 100;
          $submitBtn.attr('disabled', shouldDisabledSubmit);
          return !shouldDisabledSubmit;
        }
        function encryptRSA(publicKey, sourceValue) {
          var encrypt = new JSEncrypt();
          encrypt.setPublicKey(publicKey);
          return encrypt.encrypt(sourceValue);
        }
        function showPasswordAndFocus() {
          $password.show();
          $password.focus();
        }
        function setSignTextByStatus(status) {
          const actionText = status === 0 ? 'up' : 'in';
          document.title = 'Sign ' + actionText + ' to CNPM';
          $('.title').text('Sign ' + actionText + ' to CNPM');
          $submitBtn.text('Sign ' + actionText);
        }
      });
    </script>
  </body>
</html>
